package org.codersunit.radboid.test;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.HashMap;
import java.util.Map;

public abstract class HtmlParser {
	public static final char OPEN_TAG = '<';
	public static final char CLOSE_TAG = '>';
	public static final char CLOSE_ELEMENT = '/';
	public static final char OPEN_ENTITY = '&';
	public static final char CLOSE_ENTITY = ';';
	public static final char NUMBER_ENTITY = '#';
	public static final char ATTR_QUOTE = '"';
	public static final char ATTR_APOS = '\'';
	public static final char ATTR_VALUE_START = '=';
	public static final char SPECIAL_ELEMENT = '!';
	public static final char COMMENT = '-';
	
	private boolean stopNext = false;
	
	private CodePointBuffer builder = null;
	private BufferedReader reader = null;
	
	/**
	 * Read a stream of characters and parses it (as simple as possible) as a html document, might not 
	 * catch all invalid html documents
	 * @param stream
	 * @return
	 */
	public boolean parse(Reader stream) {
		reader = new BufferedReader(stream);
		try {
			int current;
			builder = new CodePointBuffer(200);
			while(!stopNext && (current = reader.read()) >= 0) {
				if(current == OPEN_TAG) {
					if(builder.length() > 0) {
						handleText(builder.toString());
						if(stopNext) {
							break;
						}
						builder.clear();
					}
					if(!parseTag()) {
						return false;
					}
				} else {
					builder.appendCodePoint(current);
				}
			}
		} catch(IOException e) {
			return false;
		}
		return true;
	}
	
	protected void stop() 
	{
		stopNext = true;
	}

	private boolean parseTag() 
	{
		try {
			reader.mark(1);
			int current = reader.read();
			if(current < 0) {
				return false;
			} else if(current == CLOSE_ELEMENT) {
				return parseCloseTag();
			} else if(current == SPECIAL_ELEMENT) {
				return parseSpecialTag();
			} else {
				reader.reset();
				return parseOpenTag();
			}
		} catch(IOException e) {
			return false;
		}
	}
	
	private boolean parseSpecialTag()
	{
		try {
			reader.mark(2);
			int current = reader.read();
			if(current == COMMENT) {
				current = reader.read();
				if(current == COMMENT) {
					return parseComment();
				} else {
					reader.reset();
				}
			}
			
			builder.clear();
			do {
				current = reader.read();
				if(current < 0) {
					return false;
				} else if(current != CLOSE_TAG) {
					builder.appendCodePoint(current);
				}
			} while(current != CLOSE_TAG);
			handleDoctype(builder.toString());
			return true;
		} catch(IOException e) {
			return false;
		}
	}
	
	private boolean parseComment() 
	{
		int first = -1;
		int second = -1;
		int third = -1;
		builder.clear();
		try {
			int current;
			do {
				current = reader.read();
				if(current < 0) {
					return false;
				} else {
					if(first >= 0) {
						builder.appendCodePoint(first);
					}
					first = second;
					second = third;
					third = current;
				}
			} while(first != COMMENT && second != COMMENT && third != CLOSE_TAG);
			reader.read();
			handleComment(builder.toString());
			return true;
		} catch(IOException e) {
			return false;
		}
	}
	
	private boolean parseCloseTag()
	{
		try {
			int current;
			builder.clear();
			do {
				current = reader.read();
				if(current < 0) {
					return false;
				} else {
					if(current != CLOSE_TAG) {
						builder.appendCodePoint(current);
					}
				}
			} while(current != CLOSE_TAG);
			handleEndElement(builder.toString());
			return true;
		} catch(IOException e) {
			return false;
		}
	}
	
	private boolean parseOpenTag()
	{
		try {
			boolean isSelfClosing = false;
			int current;
			builder.clear();
			do {
				current = reader.read();
				if(current < 0) {
					return false;
				} else {
					if(!Character.isWhitespace(current) && current != CLOSE_ELEMENT && current != CLOSE_TAG) {
						builder.appendCodePoint(current);
					} else if(current == CLOSE_ELEMENT) {
						isSelfClosing = true;
					}
				}
			} while(!Character.isWhitespace(current) && current != CLOSE_ELEMENT && current != CLOSE_TAG);
			
			if(isSelfClosing || current == CLOSE_TAG) {
				handleStartElement(builder.toString(), new HashMap<String, String>(), isSelfClosing);
				return true;
			} else {
				return parseTagAttributes(builder.toString());
			}
		} catch(IOException e) {
			return false;
		}
	}
	
	private boolean parseTagAttributes(String tagName)
	{
		try {
			int current;
			builder.clear();
			HashMap<String, String> attrs = new HashMap<String, String>();
			boolean lastWasClose = false;
			
			do {
				current = reader.read();
				if(current < 0) {
					return false;
				} else {
					if(current == CLOSE_ELEMENT || current == CLOSE_TAG) {
						attrs.put(builder.toString(), null);
					} else if(current == ATTR_VALUE_START) {
						String name = builder.toString();
						current = reader.read();
						if(current == ATTR_QUOTE && !parseToNext(ATTR_QUOTE)) {
							return false;
						} else if(current == ATTR_APOS && !parseToNext(ATTR_APOS)) {
							return false;
						} else if(current != ATTR_QUOTE && current != ATTR_APOS && !parseToWhitespace()) {
							return false;
						}
						attrs.put(name, builder.toString());
						builder.clear();
					} else if(!Character.isWhitespace(current)) {
						builder.appendCodePoint(current);
					}
					
					if(current == CLOSE_ELEMENT) {
						lastWasClose = true;
					} else if(!Character.isWhitespace(current) && current != CLOSE_TAG) {
						lastWasClose = false;
					}
					
				}
			} while(current != CLOSE_TAG);
			handleStartElement(tagName, attrs, lastWasClose);
		} catch(IOException e) {
			return false;
		}
		return true;
	}
	
	private boolean parseToNext(char ch)
	{
		builder.clear();
		try {
			int current;
			do {
				current = reader.read();
				if(current < 0) {
					return false;
				} else if(current != ch) {
					builder.appendCodePoint(current);
				}
			} while(current != ch);
			return true;
		} catch(IOException e) {
			return false;
		}
	}
	
	private boolean parseToWhitespace()
	{
		builder.clear();
		try {
			int current;
			do {
				current = reader.read();
				if(current < 0) {
					return false;
				} else if(!Character.isWhitespace(current)) {
					builder.appendCodePoint(current);
				}
			} while(!Character.isWhitespace(current));
			return true;
		} catch(IOException e) {
			return false;
		}
	}
	
	protected abstract void handleStartElement(String name, Map<String, String> attrs, boolean selfClosed);
	
	protected abstract void handleEndElement(String name);
	
	protected abstract void handleText(String text);
	
	protected abstract void handleDoctype(String doctype);
	
	protected abstract void handleComment(String comment);
	
	public class CodePointBuffer {
		private char[] storage;
		private int current;
		
		public CodePointBuffer(int capacity) {
			storage = new char[capacity];
			current = 0;
		}
		
		public void expand(int minNewSize) {
			int newsize = storage.length;
			while(newsize <= minNewSize) {
				newsize = newsize * 2;
			}
			char[] newStorage = new char[newsize];
			System.arraycopy(storage, 0, newStorage, 0, current);
			storage = newStorage;
		}
		
		public void clear() {
			current = 0;
		}
		
		public void appendCodePoint(int code) {
			if(current >= storage.length) {
				expand(current);
			}
			storage[current] = (char) code;
			current += 1;
		}
		
		@Override
		public String toString() {
			return new String(storage, 0, current);
		}
		
		public int length() {
			return current;
		}
	}
}
